TypeScript · 9274 bytes Raw Blame History
1 'use client';
2
3 import { useState, useEffect } from 'react';
4 import Link from 'next/link';
5 import { useParams } from 'next/navigation';
6 import { getPersonDetail, PersonDetailWithContributions } from '@/lib/api';
7 import Header from '@/components/Header';
8 import ContributionForm from '@/components/ContributionForm';
9 import ContributionDisplay from '@/components/ContributionDisplay';
10
11 export default function PersonPage() {
12 const params = useParams();
13 const personId = parseInt(params.id as string);
14
15 const [person, setPerson] = useState<PersonDetailWithContributions | null>(null);
16 const [loading, setLoading] = useState(true);
17 const [error, setError] = useState<string | null>(null);
18 const [pdfError, setPdfError] = useState(false);
19 const [pdfUrl, setPdfUrl] = useState<string | null>(null);
20 const [loadingPdf, setLoadingPdf] = useState(false);
21
22 useEffect(() => {
23 async function fetchData() {
24 try {
25 const personData = await getPersonDetail(personId);
26 setPerson(personData);
27 } catch (err) {
28 setError(err instanceof Error ? err.message : 'Failed to load person details');
29 console.error(err);
30 } finally {
31 setLoading(false);
32 }
33 }
34
35 fetchData();
36 }, [personId]);
37
38 useEffect(() => {
39 async function fetchPdfUrl() {
40 if (person && person.pdf_key && !pdfUrl) {
41 setLoadingPdf(true);
42 try {
43 const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
44 setPdfUrl(`${apiUrl}/memorial/persons/${person.id}/pdf/`);
45 } catch (err) {
46 console.error('Failed to set PDF URL:', err);
47 setPdfError(true);
48 } finally {
49 setLoadingPdf(false);
50 }
51 }
52 }
53
54 fetchPdfUrl();
55 }, [person, pdfUrl]);
56
57 const handleContributionSuccess = async () => {
58 // Reload person data to get updated contributions
59 try {
60 const personData = await getPersonDetail(personId);
61 setPerson(personData);
62 } catch (err) {
63 console.error('Failed to reload person data:', err);
64 }
65 };
66
67 if (loading) {
68 return (
69 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
70 <p className="text-gray-600 text-xl">Loading...</p>
71 </div>
72 );
73 }
74
75 if (error || !person) {
76 return (
77 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
78 <div className="text-center">
79 <p className="text-red-600 mb-4 text-xl">{error || 'Person not found'}</p>
80 <Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold">
81 Return to Home
82 </Link>
83 </div>
84 </div>
85 );
86 }
87
88 const displayName = (() => {
89 const name = person.full_display_name || person.display_name;
90 if (person.rank && person.full_display_name) {
91 return name.replace(person.rank + ' ', '').replace(person.rank + ', ', '');
92 }
93 return person.full_display_name ? name : person.display_name;
94 })();
95
96 return (
97 <div className="min-h-screen bg-vmi-cream">
98 <Header
99 breadcrumbs={[
100 { label: 'Home', href: '/' },
101 { label: person.conflict_name, href: `/memorial/conflict/${person.conflict}` },
102 { label: person.display_name }
103 ]}
104 />
105
106 {/* Main Content */}
107 <main className="max-w-6xl mx-auto px-4 py-12">
108 {/* Person Header */}
109 <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
110 <h1 className="text-4xl font-black text-vmi-red mb-2 flex items-center gap-2">
111 {displayName}
112 {person.pdf_key && (
113 <span title="Memorial document available" className="inline-block align-middle">
114 {/* Simple document icon SVG */}
115 <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#b91c1c" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-file-text">
116 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
117 <polyline points="14 2 14 8 20 8" />
118 <line x1="16" y1="13" x2="8" y2="13" />
119 <line x1="16" y1="17" x2="8" y2="17" />
120 <line x1="10" y1="9" x2="9" y2="9" />
121 </svg>
122 </span>
123 )}
124 </h1>
125
126 {/* Rank and Unit subtitle */}
127 {(person.rank || person.unit) && (
128 <div className="mb-6">
129 {person.rank && (
130 <p className="text-xl font-bold text-gray-700">{person.rank}</p>
131 )}
132 {person.unit && (
133 <p className="text-lg text-gray-600 italic">{person.unit}</p>
134 )}
135 </div>
136 )}
137
138 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-gray-800">
139 <div className="space-y-3">
140 {person.class_year && (
141 <p className="text-lg">
142 <span className="font-bold text-gray-700">Class Year:</span> {person.class_year}
143 </p>
144 )}
145 <p className="text-lg">
146 <span className="font-bold text-gray-700">Conflict:</span> {person.conflict_name}
147 </p>
148 </div>
149 <div className="space-y-3">
150 {person.death_date_display && (
151 <p className="text-lg">
152 <span className="font-bold text-gray-700">Date of Death:</span>{' '}
153 {person.death_date_display}
154 </p>
155 )}
156 </div>
157 </div>
158 </div>
159
160 {/* Death Description Section */}
161 {person.death_description && (
162 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-12 shadow-xl">
163 <h2 className="text-2xl font-bold mb-4 text-vmi-red">
164 Circumstances of Death
165 </h2>
166 <p className="text-lg text-gray-800 leading-relaxed italic">
167 {person.death_description}
168 </p>
169 </div>
170 )}
171
172 {/* PDF Viewer */}
173 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mb-12">
174 <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
175 Memorial Portrait
176 </h2>
177
178 {person.pdf_key ? (
179 <div className="relative">
180 {loadingPdf ? (
181 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
182 <p className="text-gray-700 text-lg">Loading PDF...</p>
183 </div>
184 ) : pdfError ? (
185 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
186 <p className="text-gray-700 mb-4 text-lg">
187 Unable to load PDF viewer.
188 </p>
189 {pdfUrl && (
190 <a
191 href={pdfUrl}
192 target="_blank"
193 rel="noopener noreferrer"
194 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red transition-colors font-semibold"
195 >
196 Open PDF in New Tab
197 </a>
198 )}
199 </div>
200 ) : pdfUrl ? (
201 <div className="border-2 border-gray-400 rounded-lg overflow-hidden">
202 <iframe
203 src={`${pdfUrl}#toolbar=0&navpanes=0`}
204 className="w-full h-[800px]"
205 onError={() => setPdfError(true)}
206 title={`Memorial document for ${person.display_name}`}
207 />
208 <div className="p-6 bg-gray-100 text-center border-t-2 border-gray-400">
209 <a
210 href={pdfUrl}
211 target="_blank"
212 rel="noopener noreferrer"
213 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red transition-colors font-semibold"
214 >
215 Open PDF in Full Screen
216 </a>
217 </div>
218 </div>
219 ) : null}
220 </div>
221 ) : (
222 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
223 <p className="text-gray-700 text-lg">
224 No memorial document available yet.
225 </p>
226 </div>
227 )}
228 </div>
229
230 {/* Community Contributions Section */}
231 <ContributionForm
232 personId={person.id}
233 personName={displayName}
234 onSuccess={handleContributionSuccess}
235 />
236
237 {/* Display Approved Contributions */}
238 {person.contributions && person.contributions.length > 0 && (
239 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mt-8">
240 <ContributionDisplay contributions={person.contributions} />
241 </div>
242 )}
243 </main>
244 </div>
245 );
246 }